package com.liuya.safe.policy.service.impl;

import com.liuya.base.AbstractBaseService;
import com.liuya.base.BaseModel;
import com.liuya.common.exception.EntityExistException;
import com.liuya.common.IOUtil;
import com.liuya.common.MyHashMap;
import com.liuya.common.StringUtil;
import com.liuya.db.DBLevelException;
import com.liuya.db.DBPower;
import com.liuya.db.script.BusinessData;
import com.liuya.safe.entitle.service.EntitleManagerService;
import com.liuya.safe.model.SafeUser;
import com.liuya.db.sql.xml.BusinessDataList;
import com.liuya.db.util.DBUtil;
import com.liuya.entitle.BusinessDataTestResult;
import com.liuya.safe.util.Constants;
import com.liuya.safe.policy.service.BusinessDataManagerService;
import com.liuya.safe.policy.service.QueryManagerService;
import com.liuya.safe.exception.AuthException;
import com.liuya.safe.model.SafeBusinessData;
import com.liuya.safe.util.Factory;
import com.liuya.db.script.ScriptFactory;
import org.springframework.stereotype.Service;

import java.io.*;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

@Service("businessDataManagerService")
public class BusinessDataManagerServiceImpl extends AbstractBaseService<SafeBusinessData> implements BusinessDataManagerService, Serializable {
    private static final String ROOT_ID = "0";
    private static String storeDir = Constants.getBusinessDataStoreDir();
    private Map storeMap;
    private String appName;
    private Comparator comp;
    private String storeFilePostfix;

    private static Log log = LogFactory.getLog(BusinessDataManagerServiceImpl.class);

    public BusinessDataManagerServiceImpl() {
    }

    public BusinessDataManagerServiceImpl(String appName) {
        this.appName = appName;
        loadIntoMemory(appName);
    }

    private synchronized void loadIntoMemory(String appName) {
        if(!StringUtil.isEmpty(appName))
            storeFilePostfix = "_" + appName + "_busi.xml";
        else
            storeFilePostfix = "_busi.xml";

        storeMap = new MyHashMap();
        comp = new Comparator() {
            public int compare(Object o1, Object o2) {
                SafeBusinessData q1 = (SafeBusinessData) o1;
                SafeBusinessData q2 = (SafeBusinessData) o2;
                return q1.getName().compareTo(q2.getName());
            }
        };

        Collection businessDataList = mapper.selectAll(null);
        storeMap.clear();
        for (Iterator iter = businessDataList.iterator(); iter.hasNext(); ) {
            SafeBusinessData businessData = (SafeBusinessData) iter.next();

            if (businessData.getIsLeaf()) {
                loadXmlContent(businessData);
            }
            storeMap.put(businessData.getId(), businessData);
        }

        // build tree
        // first, add a root node
        SafeBusinessData root = new SafeBusinessData();
        root.setId(ROOT_ID);
        root.setPid("-1");
        root.setName("root");
        root.setIsLeaf(false);
        storeMap.put(ROOT_ID, root);

        for (Iterator iter = businessDataList.iterator(); iter.hasNext(); ) {
            SafeBusinessData current = (SafeBusinessData) iter.next();
            // set parent
            String pid = current.getPid();
            SafeBusinessData pNode = (SafeBusinessData) storeMap.get(pid);
            current.setParent(pNode);

            // set as a child
            if (pNode != null) {
                Collection children = pNode.getChildren();
                if (children == null) {
                    children = new LinkedList();
                    pNode.setChildren(children);
                }
                children.add(current);
            }
        }
    }

    private void loadXmlContent(SafeBusinessData businessData) {
        // Read file system to receive xml content
        String file = businessData.getFileName();
        if (!StringUtil.isEmpty(file)) {
            String storeFile = Constants.getBusinessDataStoreDir() + file;
            try {
                String content = IOUtil.read(storeFile);
                businessData.setXmlContent(content);
            } catch (IOException e) {
                log.error("", e);
                throw new AuthException(e);
            }
        }

        String xmlContent = businessData.getXmlContent();
        if (!StringUtil.isEmpty(xmlContent)) {
            com.liuya.db.sql.xml.BusinessDataType xmlBusinessData;
            try {
                xmlBusinessData = com.liuya.db.sql.xml.BusinessData.unmarshal(new StringReader(xmlContent));
            } catch (Exception e) {
                log.error("", e);
                throw new AuthException(e);
            }
            BusinessData scriptBusinessData = ScriptFactory.getBusinessData(xmlBusinessData, Factory.getQueryManager(appName, "queryManagerService"));
            businessData.setScriptBusinessData(scriptBusinessData);
        }
    }

    public Collection checkSameNameBusinessData(String fileUrl) {
        try {
            BusinessDataList root = BusinessDataList.unmarshal(new FileReader(fileUrl));
            com.liuya.db.sql.xml.BusinessData[] xmlDataList = root.getBusinessData();
            return checkSameNameBusinessData(xmlDataList);
        } catch (Exception e) {
            log.error("", e);
            throw new AuthException(e);
        }
    }

    private Collection checkSameNameBusinessData(com.liuya.db.sql.xml.BusinessData[] dataList) {
        // put to be checked name into set
        Set toCheckNames = new HashSet();
        for (int i = 0; i < dataList.length; i++) {
            String name = dataList[i].getName();
            toCheckNames.add(name);
        }

        // compare every node name in store with toCheckNames
        List sameNames = new ArrayList(dataList.length);
        for (Iterator iter = storeMap.values().iterator(); iter.hasNext(); ) {
            SafeBusinessData data = (SafeBusinessData) iter.next();
            String name = data.getName();
            if (data.getIsLeaf() && toCheckNames.contains(name)) {
                sameNames.add(name);
            }
        }

        return sameNames;
    }

    public void installBusinessData(String fileUrl, boolean overwrite) {
        Connection conn = null;
        try {
//            conn = DBPower.getConnection(table.getId());
            conn = DBPower.getDefaultConnection();
            BusinessDataList root = BusinessDataList.unmarshal(new FileReader(fileUrl));
            com.liuya.db.sql.xml.BusinessData[] xmlDataList = root.getBusinessData();
            List saveUcs = new ArrayList(xmlDataList.length);
            Collection sameNames = checkSameNameBusinessData(xmlDataList);
            Set sameNameSet = new HashSet();
            sameNameSet.addAll(sameNames);
            for (int i = 0; i < xmlDataList.length; i++) {
                com.liuya.db.sql.xml.BusinessData xmlData = xmlDataList[i];
                String name = xmlData.getName();
                java.io.StringWriter xmlContentWriter = new java.io.StringWriter();
                xmlData.marshal(xmlContentWriter);
                SafeBusinessData data = new SafeBusinessData();

                data.setName(name);
                data.setXmlContent(xmlContentWriter.toString());
                data.setInstallDate(new Date());
                data.setPid(ROOT_ID);// AS a child of ROOT node
                data.setIsLeaf(true);

                if (sameNameSet.contains(name)) {
                    if (overwrite) {
                        // update this BusinessData
                        SafeBusinessData orginal = getBusinessDataByName(name);
                        data.setId(orginal.getId());
                        data.setFileName(data.getId() + storeFilePostfix);
                        data.setIsLeaf(orginal.getIsLeaf());
                        data.setPid(orginal.getPid());
                        mapper.updateByPrimaryKey(data);
                    }
                } else {
                    data.setId(newBusinessDataId());
                    data.setFileName(data.getId() + storeFilePostfix);
                    saveUcs.add(data);
                }

                // write xml content into file system
                String storeFile = Constants.getBusinessDataStoreDir() + data.getFileName();
                IOUtil.write(storeFile, data.getXmlContent());
            }
//            saver.batchSave(conn, saveUcs);
            mapper.batchSave(saveUcs);

            // definition changed, reload
            loadIntoMemory(appName);
        } catch (DBLevelException e) {
            log.error("", e);
            try {
                conn.rollback();
            } catch (SQLException e1) {
                throw new AuthException(e1);
            }
            throw new AuthException(e);
        } catch (IOException e) {
            log.error("", e);
            try {
                conn.rollback();
            } catch (SQLException e1) {
                throw new AuthException(e1);
            }
            throw new AuthException(e);
        } catch (Exception e) {
            log.error("", e);
            throw new AuthException(e);
        } finally {
            DBUtil.close(conn);
        }
    }

    public Collection getLikelyBusinessData(String name) {
        List result = new LinkedList();
        for (Iterator iter = storeMap.values().iterator(); iter.hasNext(); ) {
            SafeBusinessData businessData = (SafeBusinessData) iter.next();
            String businessDataName = businessData.getName();
            if (businessDataName.indexOf(name) > -1) {
                result.add(businessData);
            }
        }
        return result;
    }

    public SafeBusinessData getBusinessDataByName(String name) {
        for (Iterator iter = storeMap.values().iterator(); iter.hasNext(); ) {
            SafeBusinessData businessData = (SafeBusinessData) iter.next();
            String businessDataName = businessData.getName();
            if (businessDataName.equals(name)) {
                return businessData;
            }
        }
        return null;
    }

    private String newBusinessDataId() {
        try {
            Long id = DBUtil.getSequenceNextVal("business_data_id");

            // we need id>0
            while (id <= 0) {
                id = DBUtil.getSequenceNextVal("business_data_id");
            }
            return id.toString();
        } catch (SQLException e) {
            throw new DBLevelException(e);
        }
    }

    /**
     * Remove xml head like <?xml version="1.0" encoding="UTF-8"?>
     */
    private String removeXMLHead(String xml) {
        int index = xml.indexOf("?>");
        if (index > 0)
            return xml.substring(index + 2);
        else
            return xml;
    }

    public Collection getAllBusinessData() {
        if(storeMap==null || storeMap.size()<1)
            loadIntoMemory(appName);
        Collection values = storeMap.values();
        List result = new ArrayList(values.size());
        result.addAll(values);
        Collections.sort(result, comp);
        return result;
    }

    public SafeBusinessData addBusinessData(String pid, String name, String description, boolean isLeaf) throws EntityExistException {
        if (isLeaf) {
            return addLeafBusinessData(pid, name, description);
        } else {
            return addBranchBusinessData(pid, name, description);
        }
    }

    private SafeBusinessData addLeafBusinessData(String pId, String name, String description) throws EntityExistException {
        SafeBusinessData businessData = new SafeBusinessData();
        businessData.setId(newBusinessDataId());
        businessData.setName(name);
        businessData.setDescription(description);
        businessData.setInstallDate(new Date());
        businessData.setPid(pId);
        businessData.setIsLeaf(true);
        businessData.setXmlContent(initXmlContent(name, description));
        businessData.setFileName(businessData.getId() + storeFilePostfix);

        // Write to file system
        String file = Constants.getBusinessDataStoreDir() + businessData.getFileName();
        try {
            IOUtil.write(file, businessData.getXmlContent());
        } catch (IOException e) {
            log.error("", e);
            throw new AuthException(e);
        }
        mapper.insert(businessData);

        // update cache
        loadXmlContent(businessData);
        SafeBusinessData parent = (SafeBusinessData) storeMap.get(businessData.getPid());
        businessData.setParent(parent);
        parent.getChildren().add(businessData);
        storeMap.put(businessData.getId(), businessData);

        return businessData;
    }

    private SafeBusinessData addBranchBusinessData(String pId, String name, String description) throws DBLevelException, EntityExistException {
        SafeBusinessData businessData = new SafeBusinessData();
        businessData.setId(newBusinessDataId());
        businessData.setName(name);
        businessData.setDescription(description);
        businessData.setInstallDate(new Date());
        businessData.setPid(pId);
        businessData.setIsLeaf(false);
        mapper.insert(businessData);

        // update cache
        SafeBusinessData parent = (SafeBusinessData) storeMap.get(businessData.getPid());
        businessData.setParent(parent);
        parent.getChildren().add(businessData);
        storeMap.put(businessData.getId(), businessData);

        return businessData;
    }

    private String initXmlContent(String name, String description) {
        return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>      \r\n" + "<businessData name=\"" + name + "\" isRawScript=\"false\">\r\n" + "    <exprGroup linker=\"AND\"/>                 \r\n" + "    <rawScript>                                 \r\n" + "        <content></content>                     \r\n" + "    </rawScript>                                \r\n" + "</businessData>";
    }

    public void deleteBusinessData(String id) {
        // Check cache store
        SafeBusinessData businessData = (SafeBusinessData) storeMap.get(id);
        if (businessData == null) {
            return;
        }

        if (businessData.getIsLeaf()) {
            deleteSingleBusinessData(id);
        } else {
            Collection children = businessData.getChildren();
            Iterator itr = children.iterator();
            String[] ids = new String[children.size()];
            for (int i = 0; i < ids.length; i++) {
                SafeBusinessData child = (SafeBusinessData) itr.next();
                ids[i] = child.getId();
            }
            for (int i = 0; i < ids.length; i++) {
                deleteBusinessData(ids[i]);
            }

            deleteSingleBusinessData(id);
        }
    }

    private void deleteSingleBusinessData(String id) {
        SafeBusinessData hint = new SafeBusinessData();
        hint.setId(id);
        mapper.deleteByPrimaryKey(hint.getId());
        // delete cascade things
        EntitleManagerService entitleManager = Factory.getEntitleManager(appName, "entitleManagerService");
        entitleManager.deleteCascadeEntitlementByBusinessData(id);

        // remove xml file
        SafeBusinessData businessData = (SafeBusinessData) storeMap.get(id);

        if (businessData != null && !StringUtil.isEmpty(businessData.getFileName())) {
            new File(Constants.getBusinessDataStoreDir() + businessData.getFileName()).delete();
        }

        // update cache
        SafeBusinessData parent = (SafeBusinessData) storeMap.get(businessData.getPid());
        businessData.setParent(null);
        businessData.setChildren(null);
        parent.getChildren().remove(businessData);
        storeMap.remove(businessData.getId());
    }

    public SafeBusinessData getBusinessData(String id) {
        if(storeMap==null || storeMap.get(id) == null){
            loadIntoMemory("");
        }
        return (SafeBusinessData) storeMap.get(id);
    }

    public void updateBusinessData(String id, String name, String description) throws EntityExistException {
        SafeBusinessData original = getBusinessData(id);
        if (original == null) {
            return;
        }
        original.setName(name);
        original.setDescription(description);

        mapper.updateByPrimaryKey(original);

        String xmlContent = original.getXmlContent();
        if (StringUtil.isEmpty(xmlContent)) {
            // no xml content to be updated
            return;
        }

        com.liuya.db.sql.xml.BusinessDataType xmlData;
        try {
            xmlData = com.liuya.db.sql.xml.BusinessData.unmarshal(new StringReader(xmlContent));
        } catch (Exception e) {
            log.error("", e);
            throw new AuthException(e);
        }
        xmlData.setName(name);

        updateBusinessData(id, (com.liuya.db.sql.xml.BusinessData) xmlData);
    }

    public void updateBusinessData(String id, com.liuya.db.sql.xml.BusinessData content) throws EntityExistException {
        SafeBusinessData original = getBusinessData(id);
        if (original == null) {
            return;
        }

        java.io.StringWriter xmlContentWriter = new java.io.StringWriter();
        try {
            content.marshal(xmlContentWriter);
        } catch (Exception e) {
            log.error("", e);
            throw new AuthException(e);
        }
        original.setXmlContent(xmlContentWriter.toString());

        // Update xml file
        String file = Constants.getBusinessDataStoreDir() + original.getFileName();
        try {
            IOUtil.write(file, StringUtil.keepSpaceInContent(original.getXmlContent(), content.getRawScript().getContent()));
        } catch (IOException e) {
            log.error("", e);
            throw new AuthException(e);
        }

        // update cache
        loadXmlContent(original);
    }

    public SafeBusinessData copyBusinessData(String sourceId, String newName, String newDescription) throws EntityExistException {
        SafeBusinessData original = getBusinessData(sourceId);
        if (original == null) {
            String msg = "No business data found to copy.";
            log.error(msg);
            throw new AuthException(msg);
        }
        if (!original.getIsLeaf()) {
            throw new AuthException("Can not copy business data group.");
        }
        SafeBusinessData businessData = new SafeBusinessData();
        businessData.setId(newBusinessDataId());
        businessData.setName(newName);
        businessData.setDescription(newDescription);
        businessData.setInstallDate(new Date());
        businessData.setFileName(businessData.getId() + storeFilePostfix);
        businessData.setPid(original.getPid());
        businessData.setIsLeaf(original.getIsLeaf());

        if (businessData.getIsLeaf()) {
            // update xml content (only name attribute been changed)
            String file = original.getFileName();
            if (!StringUtil.isEmpty(file)) {
                String storeFile = Constants.getBusinessDataStoreDir() + file;

                try {
                    String content = IOUtil.read(storeFile);
                    businessData.setXmlContent(content);
                } catch (IOException e) {
                    log.error("", e);
                    throw new AuthException(e);
                }
            }

            String xmlContent = businessData.getXmlContent();
            if (!StringUtil.isEmpty(xmlContent)) {
                com.liuya.db.sql.xml.BusinessDataType xmlBusinessData;
                try {
                    xmlBusinessData = com.liuya.db.sql.xml.BusinessData.unmarshal(new StringReader(xmlContent));
                    xmlBusinessData.setName(businessData.getName());

                    java.io.StringWriter xmlContentWriter = new java.io.StringWriter();
                    try {
                        xmlBusinessData.marshal(xmlContentWriter);
                    } catch (Exception e) {
                        log.error("", e);
                        throw new AuthException(e);
                    }
                    businessData.setXmlContent(xmlContentWriter.toString());

                    // update to file system
                    file = Constants.getBusinessDataStoreDir() + businessData.getFileName();
                    IOUtil.write(file, StringUtil.keepSpaceInContent(businessData.getXmlContent(), xmlBusinessData.getRawScript().getContent()));
                } catch (Exception e) {
                    log.error("", e);
                    throw new AuthException(e);
                }
            }
        }
        mapper.insert(businessData);

        // update cache
        loadXmlContent(businessData);
        SafeBusinessData parent = (SafeBusinessData) storeMap.get(businessData.getPid());
        businessData.setParent(parent);
        parent.getChildren().add(businessData);
        storeMap.put(businessData.getId(), businessData);

        return businessData;
    }

    public SafeBusinessData getBusinessDataTree() {
        return getBusinessData(ROOT_ID);
    }

    public void moveBusinessData(String id, String newPid) {
        try {
            SafeBusinessData businessData = getBusinessData(id);
            SafeBusinessData oldParent = getBusinessData(businessData.getPid());
            SafeBusinessData newParent = getBusinessData(newPid);

            if (newParent.getIsLeaf()) {
                throw new AuthException("The target node may not be a leaf.");
            }
            if (businessData == newParent || isCascadeChild(businessData.getId(), newParent.getId())) {
                throw new AuthException("This node noving will produce a cycle.");
            }
            // update this node
            businessData.setPid(newParent.getId());
            mapper.updateByPrimaryKey(businessData);

            // update cache
            businessData.setParent(newParent);
            oldParent.getChildren().remove(businessData);
            newParent.getChildren().add(businessData);
        } catch (Exception e) {
            log.error("", e);
            throw new AuthException(e);
        }
    }

    private boolean isCascadeChild(String pId, String id) {
        BaseModel parent = getBusinessData(pId);
        BaseModel child = getBusinessData(id);

        if (parent != null && child != null) {
            while (child.getParent() != null) {
                if (parent == child.getParent())
                    return true;
                else
                    child = child.getParent();
            }
        }
        return false;
    }

    public BusinessDataTestResult testBusinessData(BusinessData scriptBusinessData, SafeUser user, Map context) {
        QueryManagerService queryManager = Factory.getQueryManager("", "queryManagerService");
        return scriptBusinessData.test(user, context, queryManager);
    }
}
